1 /** 2 Copyright: Copyright (c) 2018, Joakim Brännström. All rights reserved. 3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0) 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 Normal appliation mode. 7 */ 8 module app_normal; 9 10 import logger = std.experimental.logger; 11 import std.algorithm : among, map, filter, copy; 12 import std.array : empty, appender, array; 13 import std.exception : collectException; 14 15 import miniorm : spinSql; 16 import my.path; 17 import my.set; 18 19 import compile_db : CompileCommandDB, toCompileCommandDB, DbCompiler = Compiler, 20 CompileCommandFilter, defaultCompilerFilter, ParsedCompileCommand; 21 22 import code_checker.cli : Config; 23 import code_checker.database : Database, TrackFile; 24 import code_checker.engine : Environment; 25 import code_checker.cache : FileStatCache, getTrackFile, isSame; 26 27 version (unittest) { 28 import unit_threaded : shouldEqual, shouldBeTrue, UnitTestException; 29 } 30 31 immutable compileCommandsFile = "compile_commands.json"; 32 33 int modeNormal(Config conf) { 34 auto fsm = NormalFSM(conf); 35 return fsm.run; 36 } 37 38 private: 39 40 /** FSM for the control flow when in normal mode. 41 */ 42 struct NormalFSM { 43 enum State { 44 init_, 45 openDb, 46 /// change the working directory of the whole program 47 changeWorkDir, 48 /// check if a DB exists at the workdir location. Affects cleanup. 49 checkForDb, 50 /// if a command is registered to generate a DB run it 51 genDb, 52 /// check if the generation of a DB went OK 53 checkGenDb, 54 /// cleanup the database 55 fixDb, 56 /// check that it went OK to perform the cleanup 57 checkFixDb, 58 runRegistry, 59 cleanup, 60 done, 61 } 62 63 struct StateData { 64 int exitStatus; 65 bool hasGenerateDbCommand; 66 bool hasCompileDbs; 67 } 68 69 State st; 70 Config conf; 71 CompileCommandDB compileDb; 72 73 /// If the compile_commands.json that is written to the file system should be deleted when code_checker is done. 74 bool removeCompileDb; 75 76 /// Root directory from which the program where initially started. 77 AbsolutePath root; 78 79 /// Exit status of used to indicate the success to the user. 80 int exitStatus; 81 82 Database db; 83 84 FileStatCache fcache; 85 86 this(Config conf) { 87 this.conf = conf; 88 } 89 90 int run() { 91 StateData d; 92 d.hasGenerateDbCommand = conf.compileDb.generateDb.length != 0; 93 d.hasCompileDbs = conf.compileDb.dbs.length != 0; 94 95 while (st != State.done) { 96 debug logger.tracef("state: %s data: %s", st, d); 97 98 st = next(st, d); 99 action(st); 100 101 // sync with changed struct members as needed 102 d.exitStatus = exitStatus; 103 } 104 105 return d.exitStatus; 106 } 107 108 /** The next state is calculated. Only dependent on current state and state data. 109 * 110 * These clean depenencies should make it easier to reason about the flow. 111 */ 112 static State next(const State curr, const StateData d) { 113 State next_ = curr; 114 115 final switch (curr) { 116 case State.init_: 117 next_ = State.openDb; 118 break; 119 case State.openDb: 120 next_ = State.changeWorkDir; 121 break; 122 case State.changeWorkDir: 123 next_ = State.checkForDb; 124 break; 125 case State.checkForDb: 126 next_ = State.fixDb; 127 if (d.hasGenerateDbCommand) 128 next_ = State.genDb; 129 break; 130 case State.genDb: 131 next_ = State.checkGenDb; 132 break; 133 case State.checkGenDb: 134 next_ = State.fixDb; 135 if (d.exitStatus != 0) 136 next_ = State.cleanup; 137 break; 138 case State.fixDb: 139 next_ = State.checkFixDb; 140 break; 141 case State.checkFixDb: 142 next_ = State.runRegistry; 143 if (d.exitStatus != 0) 144 next_ = State.cleanup; 145 break; 146 case State.runRegistry: 147 next_ = State.cleanup; 148 break; 149 case State.cleanup: 150 next_ = State.done; 151 break; 152 case State.done: 153 break; 154 } 155 156 return next_; 157 } 158 159 void act_openDb() { 160 import std.datetime : dur; 161 import code_checker.database; 162 163 try { 164 db = Database.make(conf.database); 165 } catch (Exception e) { 166 logger.warning(e.msg); 167 } 168 169 try { 170 db.compileDbTrackApi.cleanup(2.dur!"weeks"); 171 } catch (Exception e) { 172 } 173 } 174 175 void act_changeWorkDir() { 176 import std.file : getcwd, chdir; 177 178 root = Path(getcwd).AbsolutePath; 179 if (conf.workDir != root) 180 chdir(conf.workDir); 181 } 182 183 void act_genDb() { 184 import std.file : exists; 185 import std.process : spawnShell, wait; 186 187 bool isUnchanged() nothrow { 188 try { 189 if (!exists(compileCommandsFile)) 190 return false; 191 if (conf.compileDb.generateDbDeps.empty) 192 return true; 193 return !isChanged(db, 194 conf.compileDb.generateDbDeps ~ AbsolutePath(compileCommandsFile), fcache); 195 } catch (Exception e) { 196 logger.trace(e.msg).collectException; 197 } 198 return false; 199 } 200 201 if (isUnchanged) 202 return; 203 204 auto res = spawnShell(conf.compileDb.generateDb).wait; 205 fcache = typeof(fcache).init; // drop cache because the update cmd may have changed a dependency 206 207 if (res == 0) { 208 updateCompileDbTrack(db, conf.compileDb.generateDbDeps, fcache); 209 } else { 210 // the user need some helpful feedback for what failed 211 logger.errorf("Failed running the command to generate %(%s, %)", conf.compileDb.dbs); 212 logger.error("Executed the following commands:"); 213 logger.error("# if this directory is wrong use --workdir", root); 214 logger.error("cd", root); 215 logger.error(conf.compileDb.generateDb); 216 exitStatus = 1; 217 } 218 } 219 220 void act_fixDb() { 221 import std.stdio : File; 222 import std.file : exists; 223 import compile_db : fromArgCompileDb; 224 225 compileDb = fromArgCompileDb(conf.compileDb.dbs.map!(a => cast(string) a.idup).array); 226 227 bool isUnchanged() nothrow { 228 try { 229 if (!exists(compileCommandsFile)) 230 return false; 231 return !isChanged(db, conf.compileDb.dbs ~ AbsolutePath(compileCommandsFile), 232 fcache); 233 } catch (Exception e) { 234 logger.trace(e.msg).collectException; 235 } 236 return false; 237 } 238 239 if (isUnchanged) 240 return; 241 242 logger.trace("Creating a unified compile_commands.json"); 243 244 try { 245 auto compile_db = appender!string(); 246 unifyCompileDb(compileDb, conf.compiler.useCompilerSystemIncludes, 247 conf.compileDb.flagFilter, compile_db); 248 File(compileCommandsFile, "w").write(compile_db.data); 249 250 fcache.drop(AbsolutePath(compileCommandsFile)); // do NOT use previously cached value 251 updateCompileDbTrack(db, conf.compileDb.dbs ~ AbsolutePath(compileCommandsFile), fcache); 252 } catch (Exception e) { 253 logger.errorf("Unable to process %s", compileCommandsFile); 254 logger.error(e.msg); 255 exitStatus = 1; 256 } 257 } 258 259 void act_runRegistry() { 260 import code_checker.engine; 261 import compile_db : fromArgCompileDb, parseFlag, CompileCommandFilter; 262 import code_checker.change : dependencyAnalyze; 263 import code_checker.engine.types : TotalResult; 264 265 auto changed = () { 266 bool[AbsolutePath] rval; 267 268 try { 269 foreach (v; dependencyAnalyze(db, AbsolutePath("."), fcache).byKeyValue) { 270 rval[v.key.AbsolutePath] = v.value; 271 } 272 } catch (Exception e) { 273 } 274 return rval; 275 }(); 276 277 Environment env; 278 env.compileDbFile = AbsolutePath(Path(compileCommandsFile)); 279 env.compileDb = compileDb; 280 env.files = () { 281 if (!conf.analyzeFiles.empty) 282 return conf.analyzeFiles.map!(a => cast(string) a).array; 283 284 string[] rval; 285 foreach (dbFile; env.compileDb) { 286 if (auto v = dbFile.absoluteFile in changed) { 287 if (*v) 288 rval ~= dbFile.absoluteFile.toString; 289 } else { 290 rval ~= dbFile.absoluteFile.toString; 291 } 292 } 293 return rval; 294 }(); 295 296 env.conf = conf; 297 298 TotalResult tres; 299 if (!env.files.empty) { 300 auto reg = makeRegistry; 301 tres = execute(env, conf.staticCode.analyzers, reg); 302 } 303 exitStatus = tres.status.among(Status.passed, Status.none) ? 0 : 1; 304 305 spinSql!(() { 306 auto trans = db.transaction; 307 try { 308 removeDroppedFiles(db, env, root); 309 removeFailing(db, root, tres.failed); 310 } catch (Exception e) { 311 logger.trace(e.msg); 312 } 313 trans.commit; 314 }); 315 316 if (!tres.success.empty) { 317 logger.trace("Saving result for ", tres.success); 318 spinSql!(() { 319 auto trans = db.transaction; 320 try { 321 saveDependencies(db, env, root, tres.success, fcache); 322 db.dependencyApi.cleanup; 323 } catch (Exception e) { 324 logger.trace(e.msg); 325 } 326 trans.commit; 327 }); 328 } 329 } 330 331 void act_cleanup() { 332 import std.file : chdir; 333 334 chdir(root); 335 } 336 337 /// Generate a callback for each state. 338 void action(const State st) { 339 string genCallAction() { 340 import std.format : format; 341 import std.traits : EnumMembers; 342 343 string s; 344 s ~= "final switch(st) {"; 345 static foreach (a; EnumMembers!State) { 346 { 347 const actfn = format("act_%s", a); 348 static if (__traits(hasMember, NormalFSM, actfn)) 349 s ~= format("case State.%s: %s();break;", a, actfn); 350 else { 351 pragma(msg, __FILE__ ~ ": no callback found: " ~ actfn); 352 s ~= format("case State.%s: break;", a); 353 } 354 } 355 } 356 s ~= "}"; 357 return s; 358 } 359 360 mixin(genCallAction); 361 } 362 } 363 364 /// Unify multiple compilation databases to one json file. 365 void unifyCompileDb(AppT)(CompileCommandDB db, const DbCompiler user_compiler, 366 CompileCommandFilter flag_filter, ref AppT app) { 367 import std.ascii : newline; 368 import std.format : formattedWrite; 369 import std.path : stripExtension; 370 import std.range : put; 371 import compile_db; 372 373 logger.trace(flag_filter); 374 375 void writeEntry(T)(T e) { 376 auto raw_flags = () @safe { 377 import std.json : JSONValue; 378 379 auto app = appender!(string[]); 380 //auto pflags = e.parseFlag(flag_filter); 381 app.put(e.flags.compiler); 382 e.flags.completeFlags.copy(app); 383 // add back dummy -c otherwise clang-tidy do not work. 384 // clang-tidy says "Passed" on everything. 385 ["-c", e.cmd.absoluteFile.toString].copy(app); 386 // correctly quotes interior strings as JSON requires. 387 return JSONValue(app.data).toString; 388 }(); 389 390 formattedWrite(app, `"directory": "%s",`, cast(string) e.cmd.directory); 391 formattedWrite(app, `"arguments": %s,`, raw_flags); 392 393 if (!e.cmd.output.empty) 394 formattedWrite(app, `"output": "%s",`, cast(string) e.cmd.absoluteOutput); 395 formattedWrite(app, `"file": "%s"`, cast(string) e.cmd.absoluteFile); 396 } 397 398 logger.trace("database ", db); 399 400 if (db.empty) 401 return; 402 auto entries = ParsedCompileCommandRange.make(db.fileRange.parse(flag_filter) 403 .addCompiler(user_compiler).replaceCompiler(user_compiler).addSystemIncludes.array) 404 .array; 405 if (entries.empty) 406 return; 407 408 formattedWrite(app, "["); 409 410 bool isFirst = true; 411 foreach (e; entries) { 412 logger.trace(e); 413 414 if (isFirst) { 415 isFirst = false; 416 } else { 417 put(app, ","); 418 put(app, newline); 419 } 420 421 formattedWrite(app, "{"); 422 writeEntry(e); 423 formattedWrite(app, "}"); 424 } 425 426 formattedWrite(app, "]"); 427 } 428 429 @(`shall quote compile_commands entries as JSON requires when the value is a string containing "`) 430 unittest { 431 import std.algorithm : canFind; 432 433 // arrange 434 enum test_compile_db = `[ 435 { 436 "directory": "dir1/dir2", 437 "arguments": [ "cc", "-c", "-DFOO=\"bar\"" ], 438 "file": "file1.cpp" 439 } 440 ]`; 441 auto db = test_compile_db.toCompileCommandDB(Path(".")); 442 // act 443 auto unified = appender!string(); 444 unifyCompileDb(db, DbCompiler.init, 445 CompileCommandFilter(defaultCompilerFilter.filter.dup, 0), unified); 446 // assert 447 try { 448 unified.data.canFind(`-DFOO=\"bar\"`).shouldBeTrue; 449 } catch (UnitTestException e) { 450 unified.data.shouldEqual("a trick to print the unified string when the test fail"); 451 } 452 } 453 454 Path toIncludePath(AbsolutePath f, AbsolutePath root) { 455 import std.algorithm : startsWith; 456 import std.path : relativePath, buildNormalizedPath; 457 458 if (f.toString.startsWith(root.toString)) 459 return relativePath(f, root).Path; 460 return f; 461 } 462 463 void saveDependencies(ref Database db, Environment env, AbsolutePath root, 464 AbsolutePath[] successFiles, ref FileStatCache fcache) { 465 import code_checker.engine.compile_db : toRange; 466 import code_checker.database : DepFile; 467 468 auto success = toSet(successFiles); 469 470 foreach (pcmd; toRange(env).filter!(a => a.cmd.absoluteFile in success)) { 471 db.fileApi.put(toIncludePath(pcmd.cmd.absoluteFile, root), 472 fcache.get(pcmd.cmd.absoluteFile).checksum, 473 fcache.get(pcmd.cmd.absoluteFile).timeStamp); 474 auto deps = depScan(pcmd, root).map!(a => DepFile(toIncludePath(a, 475 root), fcache.get(a).checksum, fcache.get(a).timeStamp)).array; 476 db.dependencyApi.set(toIncludePath(pcmd.cmd.absoluteFile, root), deps); 477 } 478 } 479 480 AbsolutePath[] depScan(ParsedCompileCommand pcmd, AbsolutePath root) { 481 import std.stdio : File; 482 import std..string : strip, startsWith, split; 483 import my.optional; 484 import my.container.vector; 485 import code_checker.change : toAbsolutePath; 486 487 Set!AbsolutePath found; 488 Vector!AbsolutePath que; 489 que.put(pcmd.cmd.absoluteFile); 490 491 void updateQueue(AbsolutePath p) { 492 if (p !in found) 493 que.put(p); 494 } 495 496 while (!que.empty) { 497 auto curr = que.back; 498 que.popBack; 499 500 try { 501 foreach (d; File(curr).byLine 502 .map!(a => a.strip) 503 .filter!(a => a.startsWith("#include")) 504 .map!(a => a.split) 505 .filter!(a => a.length >= 2) 506 .map!(a => a[1]) 507 .filter!(a => a.length >= 3) 508 .map!(a => strip(a.idup)[1 .. $ - 1].Path) 509 .map!(a => toAbsolutePath(a, pcmd.cmd.absoluteFile.dirName.AbsolutePath, 510 pcmd.cmd.directory, pcmd.flags.includes, pcmd.flags.systemIncludes)) 511 .filter!(a => a.hasValue) 512 .map!(a => a.orElse(AbsolutePath.init))) { 513 updateQueue(d); 514 found.add(d); 515 } 516 } catch (Exception e) { 517 logger.trace(e.msg); 518 } 519 } 520 521 return found.toArray; 522 } 523 524 void removeDroppedFiles(ref Database db, Environment env, AbsolutePath root) { 525 auto current = env.compileDb.map!(a => a.absoluteFile.toIncludePath(root)).toSet; 526 auto dbFiles = db.fileApi.getFiles.toSet; 527 foreach (removed; dbFiles.setDifference(current).toRange) { 528 db.fileApi.removeFile(removed); 529 } 530 } 531 532 void removeFailing(ref Database db, AbsolutePath root, AbsolutePath[] failing) { 533 import std.path : relativePath, buildNormalizedPath; 534 535 foreach (a; failing) { 536 db.fileApi.removeFile(relativePath(a, root).Path); 537 } 538 } 539 540 bool isChanged(ref Database db, AbsolutePath[] files, ref FileStatCache fcache) nothrow { 541 foreach (a; toSet(files).toRange) { 542 try { 543 logger.trace("checking ", a); 544 const prev = db.compileDbTrackApi.get(a); 545 const res = isSame(prev, a, fcache); 546 logger.tracef(!res, "%s is %s (prev:%s curr:%s)", a, res 547 ? "unchaged" : "changed", prev, fcache.get(a)); 548 if (!res) 549 return true; 550 } catch (Exception e) { 551 logger.trace(e.msg).collectException; 552 return true; 553 } 554 } 555 return false; 556 } 557 558 void updateCompileDbTrack(ref Database db, AbsolutePath[] files, ref FileStatCache fcache) nothrow { 559 foreach (a; toSet(files).toRange) { 560 try { 561 auto d = fcache.get(a); 562 db.compileDbTrackApi.put(d); 563 logger.tracef("saved track data for %s %s", a, d); 564 } catch (Exception e) { 565 logger.trace(e.msg).collectException; 566 } 567 } 568 }